Skip to content

fix(sandbox): kill in-container process group on command timeout - #107

Merged
jkyberneees merged 1 commit into
mainfrom
fix/sandbox-timeout-kill
Jul 26, 2026
Merged

fix(sandbox): kill in-container process group on command timeout#107
jkyberneees merged 1 commit into
mainfrom
fix/sandbox-timeout-kill

Conversation

@jkyberneees

Copy link
Copy Markdown
Contributor

Summary

Fixes the reported finding (verified against current code and a live container before fixing):

In-container processes survive shell-command timeout — LOW. When a sandboxed command times out, the host-side docker exec client is killed, but Docker does not propagate the signal — the in-container process keeps running (CPU/memory, half-written files in /workspace) until the container is torn down at session end. Repeated timeouts can accumulate runaway processes inside the container.

Change

shellTool and parallelShellTool now run sandboxed commands through wrapSandboxCommand (cmd/odek/shell.go):

  • Pid-marker wrapper: docker exec -w /workspace <c> sh -c 'echo $$ > /tmp/.odek-cmd-<pid>-<seq>.pid; sh -c "$1"; rc=$?; rm -f <pidfile>; exit $rc' odek-cmd <command>. The command string travels as positional argument $1 and is never interpolated into the wrapper, so quoting cannot break out of it.
  • Group kill follow-up: verified empirically that docker exec processes are their own process-group leaders (pgid == pid) and that forked children share the group. On timeout or cancellation, odek follows up with docker exec <c> sh -c 'kill -KILL -$(cat <pidfile>) 2>/dev/null; rm -f <pidfile>' — tearing down the command and its forked children (sleep 300 & sleep 300 dies completely). Previously it lingered until session end.
  • Best-effort, documented: children that call setsid/setpgid escape the group; the follow-up no-ops if the container is already gone; the pidfile self-cleans on normal exit and lives on a per-container tmpfs otherwise.

Scope note: the mitigation's "container restart after N stale kills" escalation is deliberately not implemented — the group-kill follow-up addresses the leak directly without mid-session container churn.

Tests

  • New E2E tests (gated on ODEK_E2E=true + Docker): TestE2E_SandboxTimeoutKillsInContainerProcesses and TestE2E_SandboxParallelTimeoutKillsInContainerProcesses assert no sleep 300 processes remain after a 1s timeout while PID 1 (sleep infinity) stays healthy. Both fail against the pre-fix code (verified by stashing the fix).
  • TestShellTool_BuildCmd_Docker updated for the new argv shape (wrapper + $1 dispatch, command never interpolated) and the returned follow-up; buildCmd callers updated for the new signature.
  • go test ./... -count=1 ✅ (28 packages), go vet ✅, golangci-lint → 0 issues ✅

Docs: AGENTS.md gains an "In-container timeout kill" hardening bullet; the stale "lingers until the container is torn down" comment in shell.go is corrected.

When a sandboxed shell command timed out (or the turn was cancelled), only
the host-side docker exec client was killed - Docker does not propagate the
signal, so the in-container process kept running (CPU/memory, half-written
files in /workspace) until the container was torn down at session end.
Repeated timeouts accumulated runaway processes inside the container.

shellTool and parallelShellTool now run sandboxed commands under a
pid-marker wrapper (wrapSandboxCommand): the wrapper records the
container-side pid of its group-leading shell in a per-invocation pidfile
under /tmp, with the command passed as $1 (never interpolated, so quoting
cannot break out). docker exec processes are their own process-group
leaders (pgid == pid, verified empirically on alpine), so on timeout/cancel
odek follows up with docker exec ... kill -KILL -<pgid>, tearing down the
command and every child it forked. Children that call setsid/setpgid
escape the group - the follow-up is best-effort, not a hard guarantee.

The "container restart after N stale kills" escalation from the report is
deliberately not implemented: the group-kill follow-up addresses the leak
directly without mid-session container churn.

E2E tests (gated on ODEK_E2E + docker) prove no in-container survivors
after shell and parallel_shell timeouts while the container init stays
healthy; both fail against the pre-fix code. Unit tests updated for the
new buildCmd signature and argv shape.
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 26, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
odek f1d44cc Commit Preview URL

Branch Preview URL
Jul 26 2026, 09:43 AM

@jkyberneees
jkyberneees merged commit 285719f into main Jul 26, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant